Add Echo-TTS (community model) - #180
Conversation
|
I had a go at it. Can use anything you want from here if it helps. |
|
@dignome appreciate it. i was all out of tokens building a SaaS. will take a look and implement. |
Design for porting Echo-TTS (jordand/echo-tts-base, 2.8B DiT + Fish S1-DAC) into audio.cpp as a community model. Architecture verified against upstream source and safetensors headers, not inferred. Key findings: - EchoDiT: 24 blocks, d=2048, joint attention, adaLN, byte-level text - Fixed 640-latent / 29.72s generation window - Blockwise path subdivides that window, does not extend it - Decode and encode need near-disjoint Fish submodules - 303.6M of the Fish checkpoint is regenerable buffers, not weights Staged M0-M4 with per-milestone gates and a hard Definition of Ready before the PR leaves draft.
- Resolve RoPE theta open question (10000.0, complex-valued, model.py:9) - Add timestep embedding formula - Warn RTF vs RTFx are inverses (tts-bench vs audio.cpp conventions) - Cite the actual schema validator for the M0 gate - Define 'cosine' precisely (flattened 1-D, with max-abs-error) - Add decomposition note: M0+M1 in one plan, M2/M3/M4 separate
13 tasks, each gated on executed evidence: - M0 (T1-2): spec v1 registration + draft PR - M1 (T3-13): converter, parity dumps, GGUF, assets, tokenizer, text/speaker encoders, 24-block DiT, dual-CFG Euler sampler, PCA inverse + Fish decode, crop, warm bench Every stage gates on cosine >= 0.999 vs PyTorch before the next begins. Task 12 requires a human ear check - tensor parity cannot catch a wrong flattening-point crop. PR stays draft through M1; cloning still needs an injected .npy until M2 lands native speaker encoding.
- Repo has no CMakePresets.json; --preset would fail. Use scripts/build_linux.sh or cmake --build build/linux-cuda-release. - Existing build tree pins CMAKE_CUDA_ARCHITECTURES=75 (Turing) on an sm_86 card. Task 13 now reconfigures to 86 before measuring RTF, otherwise the number is invalid. - Note AUDIOCPP_MODEL_SET=full so the family compiles in automatically.
Baseline on this branch is registered_loaders=42, verified. Also note that a 'requires a schema v1 model contract' failure means a stale binary, not a broken tree.
Plan Task 1. Spec-backed loader (no loader.cpp), schema_version 1, capabilities.clone deliberately omits long_form until M3 earns it. Verified by execution: registered_loaders 42 -> 43, echo_tts appears as 'clon (offline)', spec parses. Fix over Codex's draft: guard used VoiceTaskKind::Tts, but the family registers as a clone task, so every real invocation would have thrown. Corrected to VoiceCloning, matching confucius4_tts:185. The registration gate could not catch this - --list-loaders enumerates loaders without constructing a session.
Documents the fixed 29.72s window, why blockwise does not extend it, the CC-BY-NC-SA output restriction (with the fish_audio precedent), benchmark provenance, options, and the WhisperD text format. Also drops an unrelated .gitignore change that was accidentally swept into an earlier docs commit, so the PR diff stays scoped.
The design spec and implementation plan are our working process, not content for audio.cpp. Preserved on the local echo-tts-planning branch and still on disk; just untracked here so the PR diff stays scoped to the actual contribution.
…rter Replaces the M0 silence stub with a complete port, contributed by @dignome and offered for use in this PR (see PR 0xShug0#180 discussion). DiT trunk, 24 blocks, joint attention with flash-attn path Byte tokenizer + WhisperD normalisation Euler dual-CFG sampler with independent text/speaker guidance PCA inverse + flattening-point crop Fish S1-DAC z_q seam, reusing the in-tree fish_audio codec GGUF converter (F16 and Q8_0) plus a manifest and verifier Long-form via the framework text chunker at 300 codepoints Scope trimmed from the source branch before landing: the root README dump, echotts-server.json (hardcoded machine paths), webui build artifacts, and a generic validate_model_spec.py that is not Echo-specific and belongs in its own PR. Two corrections on top of the contributed tree: resolve_reference_max_samples' comment claimed it falls back to the trained maximum; it returns kDefaultReferenceMaxSamples (15 s). The code is intentional and the spec publishes 15.0 in both scopes, so the comment was the error, not the behaviour. The status table still carried this PR's original milestone list, which said M2 was not started and that cloning needed a pre-computed speaker latent. session.cpp calls codec_->encode_zq directly, so both claims were false. Rewritten to separate what is implemented from what is numerically verified, because nothing in the ggml graph has been checked against PyTorch yet. Co-authored-by: dignome <dignome@gmail.com>
Independent reviews by Codex and Grok both landed on this feature, from
different angles. Codex supplied the mechanism.
The code defaulted the generation window to a text-length estimate,
justified in-comment as "an under-estimate costs time, never fidelity",
because a missing flattening point retries at full length and
generate_torch_cuda_randn is a sequential Philox stream, so the retry
draws bit-identical noise.
The noise claim is true. The fidelity conclusion does not follow, because
the seed is not what changes. Echo's generated self-attention is fully
non-causal -- model.py:249 is
self_mask = torch.ones((batch_size, seq_len), dtype=torch.bool, ...)
so every latent position attends across the whole window. Shrinking 640
to 128 changes the computation at every retained position, not merely how
many positions survive. The reference defaults to 640 (inference.py:353).
The retry also only fires when no flattening point is found, so a short
window that happens to produce a plausible flat tail is never corrected
and silently ships different audio.
Inverted to AUDIOCPP_ECHO_TTS_ADAPTIVE_WINDOW=1. The cost saving is real
and the implementation stays; only the default changes, until it has been
A/B'd against the full window on a fixed seed.
Codex also cleared the four highest-risk items the original author flagged
as possibly-silently-wrong, against the reference: half-head RoPE rotates
the first heads/2 on the head axis (model.py:199,217), GGML_ROPE_TYPE_NORMAL
is the correct interleaved convention against model.py:21's reshape(...,-1,2)
pairing, the speaker patchify reshape produces frame-then-channel ordering
matching model.py:458, and the adaLN chunk order is shift/scale/gate per
model.py:64. Tensor names remain unproven pending a real checkpoint.
Adds the models.md row, with attribution to @dignome for the implementation. Flagged by review as the one doc that was never updated.
Executed on an RTX 3090 (sm_86) against a locally converted F16 GGUF. Conversion reports manifest OK, the verifier passes, and generation round-trips through faster-whisper at 0.0% WER on 32 words. Throughput is RTF 0.86 cold, including the 5.5 GB load. A 0% WER rules out the silent-wrong failure modes -- half-head RoPE, rotary pairing, patchify layout and adaLN chunk order would each produce fluent but incorrect speech. It is still not per-tensor parity, so the missing evidence is listed explicitly rather than implied: no cosine gate against PyTorch, no flash-attn A/B, no fish_audio regression test for the restructured build_decode_quantizer, no F16-vs-Q8 listen, and no registered C++ tests.
10e833e to
1a9619a
Compare
|
@dignome this was extremely useful — thank you. I've integrated it and it's now the body of this PR, with your commits preserved as It works. Converted an F16 GGUF locally from
Sounds good too, not just measurable-good. I ran it past Codex and Grok before adopting. Codex checked your five "most likely to be wrong" items against the reference PyTorch line by line and cleared four of them — half-head RoPE on the head axis ( One real find, and both reviewers hit it independently. The adaptive generation window defaulted on, with the reasoning that an under-estimate "costs time, never fidelity" because the retry reuses the Philox stream. The noise argument is correct but the conclusion doesn't follow, because the seed isn't what changes — Trimmed for upstream scope: the root README section, Still draft. Missing a real parity pass against PyTorch, and a Genuinely appreciate you publishing it and offering it up instead of racing a competing PR. hats off to you sir @dignome |
The contributed port's host-side units were verified by hand and never
committed as tests. This registers them as a CPU test target that needs
neither a GPU nor the 5.5 GB checkpoint, so CI can hold them.
Coverage:
WhisperD normalisation, including the asymmetric double-quote rewrite
upstream applies to U+201D but not U+201C. That asymmetry looks like
a bug and is load-bearing -- "fixing" it silently desyncs the token
stream from the reference, so the test pins it.
Byte tokenisation: exact token counts and prefixes, [S1] tagging,
bracket/paren suppression, truncation at 768 including the BOS.
PCA forward/inverse round trip on an orthonormal basis, plus a
separate assertion that latent_scale is applied, so dropping it on
either leg fails rather than cancelling out.
find_flattening_point on three cases: a mid-sequence flattening, a
latent that never flattens, and one flat from frame zero.
Every expected value was produced by executing the reference
implementation (inference.py tokenizer_encode / find_flattening_point),
not by reasoning about what it should return.
Verified by mutation rather than by the tests merely passing: removing
the colon rewrite fails the normalisation case, and dropping the inverse
PCA scale fails the round trip at element 0. Both reverted.
The design spec and implementation plan are working process, not content for audio.cpp; a git add -A swept them in along with a local .gitignore edit. Files stay on disk and untracked.
Adds the numerical parity evidence the PR was missing. A 0% WER shows the
pipeline is right end to end; it does not show the DiT graph matches
PyTorch, which is what the silent-wrong failure modes would break.
tests/echo_tts/echo_tts_dit_parity.cpp
Loads the GGUF, injects the reference's own text ids, mask and
speaker latent, and scores by cosine plus max-absolute-error.
Cosine alone hides a uniform scale error and max-abs alone is
dominated by one outlier, so both are reported.
Not registered with add_test -- it needs a 5.5 GB GGUF and a
PyTorch dump, so it is hand-driven like dots_tts_vocoder_parity.
tools/community_models/echo_tts_pack_reference.py
Packs echo_ref.npz into a flat binary so the harness needs no npz
parser in C++.
EchoDitRuntime::denoise_once(x, t, lanes)
A testing seam. sample() alone cannot isolate a wrong block from a
wrong integration step; feeding the reference's own x and t makes
any difference attributable to the graph.
Result on an RTX 3090 against the F16 GGUF, reference dumped from
upstream at a fixed seed and a fixed timestep t=0.7:
denoiser cosine=0.999976711 max_abs=0.086061 rms=0.008939 PASS
That clears the 0.999 gate and settles the four items flagged as
possibly-silently-wrong: half-head RoPE, the rotary pairing convention,
the speaker patchify reshape and the adaLN chunk order all now have a
number behind them rather than a code reading.
The full 40-step trajectory from our own seeded noise scores cosine
0.905, below the gate. Not yet reported as pass or fail: the harness now
also runs the sampler from the reference's OWN initial noise, which
discriminates between RNG divergence and a real integration defect. That
run is queued behind an unrelated tts-bench job holding the GPU.
The seam recomputed sequence_length as x.size() / (latent_size * lanes), but x is always a SINGLE lane: sampler.cpp calls denoise(x_t, t, 3) with an x_t of exactly `elements` and expects elements * 3 back. `lanes` selects the width of the OUTPUT, not the input. The three-lane path therefore threw on a non-divisible size instead of running. Caught by executing the parity harness, not by reading it.
… cosines Codex and Grok reviewed the verification commits (not dignome's port) and converged on the same four things. All are addressed here. Parity harness -------------- max-absolute error was computed, printed, and never gated -- `actual = 1000 * expected` scored cosine 1.0 and PASSed. Both checks now require cosine AND max-abs, with `--denoiser-max-abs` / `--sampler-max-abs`. Verified by running the denoiser probe at `--denoiser-max-abs 0.001`: cosine still 0.999999, verdict FAIL. The header claimed the probe attributes any difference "to the graph alone". It does not: `prepare_conditioning()` runs this port's own text encoder, speaker encoder and KV projections, so the number covers the combined conditioning-plus-denoiser path. Corrected in the header and the model doc. The bundle reader took signed name/element lengths straight from the file into `resize()` and treated every dtype tag other than 1 as float. Bounded and validated; the little-endian assumption is now stated rather than implied. Host unit tests --------------- The PCA fixture was a square identity, which is its own transpose, so a transposed basis read passed, and a mean or scale dropped on *both* legs cancelled in the round trip. Replaced with a rectangular, non-symmetric orthonormal basis (2 components over 4 features), with projection and inversion each pinned against independently computed values -- confirmed against numpy. The round trip is now exact to 1e-6 on an in-subspace vector rather than 1e-3 on a bijection. Token tests checked a length and a 12-id prefix; a length-preserving rewrite (signed-char sign extension on multibyte UTF-8) passed. Full id vectors are now pinned for all five cases, generated by executing `tokenizer_encode`. The flattening fixtures were all extreme active-or-zero, so an implementation that merely searched for an all-zero window passed without evaluating either threshold. Added a quiet-but-non-zero tail (0.02 -> 30) and a flat-but-loud tail (0.5 -> 60), both confirmed against `find_flattening_point`. `require_close` compares `fabs(a - b) > tolerance`, which is false for NaN, so NaN passed every float assertion. Wrapped locally with an explicit finite check rather than changing shared test code. Also added `pad_to_max` coverage and truncation *content* -- previously only its length was asserted. Mutation-checked, each reverted after: transposed basis, mean dropped on both legs, scale dropped on both legs, and zero-window search instead of the thresholds. All four now fail the suite; all four passed it before. The sampler residual -------------------- Re-dumping the reference at float16 to match the GGUF (it defaults to bfloat16) moves the denoiser probe from cosine 0.999977 to 0.999999188 and the 40-step trajectory from 0.905481 to 0.976972. Combined with the earlier step-count sweep -- 0.9965 at 4 steps, 0.9055 at 40 -- and Codex finding no defect in a line-by-line read of the sampler against inference.py, the residual is accumulating per-step rounding amplified by dual CFG at 3.0/8.0, not a structural defect. That is an explanation, not a proof: the trajectory is still reported as below the gate. Documentation ------------- docs/community_models/echo_tts.md claimed "No cosine >= 0.999 comparison" and "No C++ unit tests are registered". Both were false at HEAD. It also used a 0 % WER on 32 words to rule out four silent-failure modes, including the speaker patchify reshape -- but WER scores words, not speaker identity, so that one could produce fluent correct text in the wrong voice and still read 0 %. Rewritten: both cosines published in one table, the trajectory marked below gate, WER demoted to what it actually shows, and the hand-run numbers separated from what CI holds. Dropped the internal planning docs again -- a third `git add -A` re-added them in 85ee6d8. Now in .git/info/exclude so it cannot recur.
…for it Echo-TTS needs continuous z_q latents from the shared Fish codec, and the way that was wired in changed the encode graph for *every* fish_audio request: `&z_q` was passed unconditionally, so an extra `ggml_sub` node was built and marked `ggml_set_output` even though fish_audio never reads it. The arithmetic was unaffected, but the allocation was not. `ggml_set_output` pins that buffer and keeps both `x` and the quantiser residual live to the end of the graph, where otherwise the residual is free to be reused after the last `quantize_one`. That is a real change to a supported core family that gains nothing from it -- and one with no test in the tree and no fish_audio checkpoint on this machine to catch it. `EncodeGraph` now takes `want_z_q`. With it false, `build_encode_quantizer` receives nullptr, no sub node is created, no output is set, and nothing is expanded onto the graph -- so the construction sequence is identical to upstream's. `encode_reference` passes false; only `encode_zq` passes true. `matches()` gained the flag so a codes-only request cannot silently reuse a z_q-bearing graph in the wrong direction. A graph that has z_q may serve a codes-only request (the codes are identical either way); the reverse forces a rebuild. In practice the two never mix on one codec instance -- fish_audio only calls encode_reference and echo_tts only calls encode_zq -- so fish_audio never gets the z_q-bearing graph at all. `read_z_q` now throws instead of dereferencing a null output tensor. The decode side needed nothing: `build_decode_quantizer` was split into `build_zq_from_codes` + `build_decode_from_zq` and reassembled as a composition of the two, which builds the same ops in the same order. The PR body called that "restructured"; it is a pure refactor and has been corrected. Verified by execution, Echo-TTS on CUDA with the F16 GGUF: RC 0, 7.82 s wall, 4.60 s of 44.1 kHz mono, peak 0.714, and an ASR round trip of **WER 0.0 %, 0 edits on 14 words**. The z_q path still produces correct speaker conditioning with the output now opt-in.
Ready for review. The DiT denoiser passes its parity gate against PyTorch; the remaining gaps are listed honestly under What's still missing, and the two open questions are structural ones only you can answer — see Scope.
Note on
ENGINE_BUILD_TESTS: a full build currently fails on threetests/moss_tts_local/*_parity.cppfiles. That is pre-existing onmain, not from this branch — those files and everymosssource here are byte-identical to upstream, and I reproduced the identical error in a cleanupstream/mainworktree configured from scratch. Flagging it so it isn't attributed to this PR.Adds Echo-TTS as a community model: an English zero-shot voice-cloning TTS model. A 2.8B diffusion transformer generates 80-D latents in PCA space, decoded to 44.1 kHz by the Fish S1-DAC autoencoder. Cloning needs a reference wav, no transcript. Text is byte-level UTF-8 — no phonemiser, no G2P, no pronunciation dependency.
Echo-TTS is on the candidate list in #34 (struck through, "contributions are welcome").
Credit
The implementation is @dignome's. They built it from this PR's design notes, published it at dignome/audio.cpp-echo-tts, and offered it here rather than opening a competing PR. I've integrated it, reviewed it, cut the parts that don't belong upstream, and fixed what the reviews found. Their commits are preserved as
Co-authored-by.Evidence
Numerical parity against PyTorch
tests/echo_tts/echo_tts_dit_parity.cppagainst a dump fromtools/community_models/echo_tts_reference.py. Gates are cosine over the flattened tensors and max-absolute-error — cosine alone cannot see a uniform scale error, and the host Philox stream matches CUDA to ~2 ULP rather than bit-exactly.The reference defaults to bfloat16; this GGUF is F16. Both dumps are shown because the difference between them turns out to be the single largest term:
The denoiser probe is the number that carries this port. It settles the four details that fail silently — half-head RoPE, the interleaved (not NEOX) rotary pairing, the speaker patchify reshape, and the adaLN
shift/scale/gateorder.It does not isolate the DiT blocks alone: the reference text ids and speaker latents are injected, but
prepare_conditioning()then runs this port's own text encoder, speaker encoder and KV projections. Enough to catch a wrong block, not enough to localise one. Per-block dumps exist in the packer (--blocks) and have not been run.The 40-step trajectory is below the gate and is reported as a FAIL of the check as written. What is established:
inference.pyfound no defect — schedule, inclusive CFG bounds, single application oftruncation_factor, three-lane CFG combination, the Euler update, and the speaker-KV boundary all agree.That is an explanation, not a proof. Treat the trajectory as unverified.
End to end
Executed on an RTX 3090 (sm_86), CUDA, F16 GGUF converted locally from
jordand/echo-tts-base+jordand/fish-s1-dac-min:manifest OK— 1117 DiT tensors, 219 blockwise tensors dropped, 495 codec tensorslatent_scalefaster-whisper-large-v3-turbo)WER on 32 words is a small sample and scores intelligibility only. It shows the pipeline runs end to end and produces the right words; it does not score speaker identity, so a wrong patchify reshape could yield fluent, correctly-worded speech in the wrong voice and still read 0 %. The denoiser cosine above is what covers that.
Generation cost is essentially constant between a 15-word and a 32-word run (7.75 s vs 7.89 s) because the window is fixed at 640 frames, so longer text inside a chunk is close to free.
Independent reviews
Two rounds, Codex (GPT-5.6) and Grok 4.6 each time — first on the contributed tree before adopting it, then on my own verification commits. Codex checked the ggml port line-by-line against the reference PyTorch; Grok judged adoption and upstream fitness.
Codex cleared the four items the original author flagged as possibly-silently-wrong: half-head RoPE rotates the first
heads/2on the head axis (model.py:199,217),GGML_ROPE_TYPE_NORMALis the correct interleaved convention againstmodel.py:21'sreshape(...,-1,2)pairing, the patchify reshape gives frame-then-channel ordering permodel.py:458, and adaLN order isshift/scale/gatepermodel.py:64. Its one CANNOT-TELL — tensor names — was settled by running the converter against the real checkpoint.Both then independently flagged the same feature, and Codex supplied the mechanism:
The adaptive generation window is now off by default. It shrank the 640-frame window to a text-length estimate, justified in-comment as costing "time, never fidelity" because a missing flattening point retries at full length on the same Philox seed. The noise claim is true; the fidelity conclusion doesn't follow, because the seed isn't what changes. Echo's generated self-attention is fully non-causal —
self_mask = torch.ones((batch_size, seq_len))atmodel.py:249— so every latent position attends across the whole window. Shrinking 640 to 128 changes the computation at every retained position, not just how many survive. And the retry only fires when no flattening point is found, so a short window that yields a plausible flat tail is never corrected. The optimisation stays, behindAUDIOCPP_ECHO_TTS_ADAPTIVE_WINDOW=1.Scope
Cut from the contributed branch before landing: the root
README.mddump,echotts-server.json(hardcoded machine paths), webuidist/andpackage-lock.jsonbuild artifacts, andvalidate_model_spec.py(a generic schema checker that isn't Echo-specific and deserves its own PR).Two things here are not additive and I'd rather flag them than have you find them:
src/models/fish_audio/codec.cpp. Echo reuses the in-tree Fish codec, which needs continuousz_qaccess. Two separate things, and I had originally described them as one:The decode side is a pure refactor.
build_decode_quantizerwas split intobuild_zq_from_codes+build_decode_from_zqand reassembled as a composition of the two. Same ops, same order, same graph. I earlier called this "restructured"; that overstated it.The encode side did change behaviour, and I've since fixed it.
&z_qwas passed unconditionally, so everyfish_audiorequest built an extraggml_subnode and marked itggml_set_outputeven thoughfish_audionever reads it. The arithmetic was unaffected, butggml_set_outputpins that buffer and keeps bothxand the quantiser residual live to the end of the graph, where otherwise the residual is free to be reused — a real allocation change for a family that gains nothing from it.EncodeGraphnow takeswant_z_q; with it false no node is built, no output is set, nothing is expanded, so the construction sequence is identical to upstream's.encode_referencepasses false, onlyencode_zqpasses true, andmatches()carries the flag so the two graphs can't be confused.That shrinks the blast radius to zero for existing
fish_audiousers, which I think is a better answer than a test proving I didn't break it. Still happy to split this into a preceding PR if you'd rather review it on its own.src/framework/audio/wav_reader.cpp(+176). PCM8/PCM32/float64/A-law/mu-law/WAVEFORMATEXTENSIBLEsupport. This is a general WAV improvement that Echo doesn't strictly need. Say the word and I'll pull it into a separate PR.Long-form and the fixed window
Echo generates at most 640 latents (640 × 2048 ÷ 44100 = 29.7215 s). Upstream's blockwise sampler subdivides that window rather than extending it (
sum(block_sizes) + continuation_length < 640), and upstream notes it "hasn't been thoroughly tested."So long text goes through the framework chunker, as the rest of the repo does —
runtime::chunk_text_requestat 300 codepoints, speaker conditioning cached once per session,append_audio_bufferto concatenate.chatterboxis the closest analogue. Dropping the blockwise path also dropslatent_encoder,wk_latentandwv_latent: the 219 tensors the converter reports discarding, ~294 M parameters.long_formis deliberately not claimed incapabilities. It appears nowhere in the C++, and only 5 of 22 TTS/clone families declare it — the 17 that don't includechatterbox,fish_audio,higgs_audio_tts,index_tts2,qwen3_tts,voxcpm2andpocket_tts. Happy to add it if you'd rather it be declared.What's still missing
fish_audioregression test, and nofish_audiocheckpoint on my machine to build one against. The z_q output is now opt-in (see Scope), sofish_audio's own graph is identical to upstream's and there is nothing left for such a test to catch — but that is an argument from construction, not from execution, and I'd rather say so than imply I ran it.AUDIOCPP_ECHO_TTS_NO_FLASH=1on a fixed seed.combine_cfg_lanesandeuler_timestep_schedulehave no registered coverage. They were checked by hand (6.6e-07 and 0.0 diff against a numpy transcription ofinference.py); that hand run is not what CI holds, and the docs now say so.Registered tests:
tests/echo_tts/echo_tts_host_units.cppviaadd_engine_unittest/add_test, CPU-only, no checkpoint needed — WhisperD normalisation, full byte-token id vectors, truncation and padding, PCA projection and inversion pinned independently against hand-computed values on a rectangular non-symmetric basis, and the flattening crop including both its thresholds. Mutation-checked against the four defects the second review said the earlier fixtures couldn't catch (transposed basis, mean dropped on both legs, scale dropped on both legs, zero-window search instead of thresholds); all four now fail the suite.Licence
Echo-TTS is CC-BY-NC-SA-4.0, and the restriction covers generated audio, not just the weights (inherited from the Fish S1-DAC dependency). Flagging it rather than leaving it to be inferred. There's in-tree precedent —
fish_audiocarries the identical restriction from the identical dependency — and audio.cpp's Apache 2.0 licence is unaffected, since weights are a separate download. Documented in the model doc so nobody ships product on non-commercial output.One question
Anything you'd want structured differently — file layout, option naming, whether the
fish_audioandwav_readerchanges should land as their own PRs first, or whether this belongs incommunity_modelsat all.Why this model
Picked by comparing every model tracked in tts-bench — 62 local TTS models benchmarked across speed, objective scores, and blind human preference — against the existing support table.
Caveats worth stating: the cloning arena averages ~30 games per model, so gaps under ~100 Elo are noise, and the ranking uses a single reference clip. Echo is top-3 on votes and 2nd on objective SIM, which are independent measurements.